package edu.gatech.gitmad;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Base64;
import android.util.Log;


/**
 * Cryptography laboratory, full of sketchy experiments yo mama didn't want you
 * to know about.
 * 
 * This class uses JCE-level ciphers to encrypt and decrypt data and whatnot. It
 * only writes and reads ciphertext to/from disk.
 * 
 * It's also used to generate SHA256 hashes and 64-bit salts derived from Java's
 * SecureRandom class, useful for password encryption.
 * 
 * Key generation: PBKDF2 with HMAC and SHA1 Cipher: AES with CBC and PKCS5
 * Padding Password hash function: SHA256
 * 
 * 
 * @author soapyKing 
 * 
 */
public class CryptoLaboratory {

	private static final String FACTORYALGORITHM = "PBKDF2WithHmacSHA1";
	private static final String CIPHERALGORITHM = "AES/CBC/PKCS5Padding";
	private static final String TAG = "CryptoLab";
	private static final String PREFS_KEY_CRYPTO_SALT = "ciphersalt";
	private static final String PREFS_KEY_IV = "iv";
	private static final String STORE_ALIAS_KEY = "key";
	private static final String STORE_NAME = "boxxy";
	private static final String ENCODING = "UTF-8";
	private static Cipher eCipher;
	private static Cipher dCipher;

	public static void initializeCiphers(char[] pass)
			throws GeneralSecurityException, IOException {

		SecretKey key = getSecretKey(pass);
		byte[] iv = null;
		if (key == null) {
			key = generateSecretKey(pass);
		} else {
			iv = getIV();
		}
		Log.d(TAG,
				"Will be using key: "
						+ Base64.encodeToString(key.getEncoded(), 0));
		
		try {
			eCipher = Cipher.getInstance(CIPHERALGORITHM);
			dCipher = Cipher.getInstance(CIPHERALGORITHM);

			eCipher.init(Cipher.ENCRYPT_MODE, key);
			AlgorithmParameters params = eCipher.getParameters();
			if (iv == null) {
				iv = params.getParameterSpec(IvParameterSpec.class).getIV();
				storeIV(iv);
			}
//			Log.d(TAG, "The iv in Base64 is: " + Base64.encodeToString(iv, 0));
			dCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
		} catch (NoSuchAlgorithmException e) {
			Log.e(TAG, "Exception initializing cipher");
			throw new GeneralSecurityException("Exception initializing cipher");
		} catch (NoSuchPaddingException e) {
			Log.e(TAG, "Exception initializing cipher");
			throw new GeneralSecurityException("Exception initializing cipher");
		} catch (InvalidKeyException e) {
			Log.e(TAG, "Exception initializing cipher");
			throw new GeneralSecurityException("Exception initializing cipher");
		}
	}

	public static boolean writePlainToCipher(String plain, File destination)
			throws FileNotFoundException, IOException,
			IllegalBlockSizeException, BadPaddingException {

		byte[] subjectBytes = plain.getBytes(ENCODING);
//		Log.d(TAG, "Our string is: " + plain);
//		Log.d(TAG, "Which is: " + plain.length() + " characters long");
//		Log.d(TAG, "Which is: " + subjectBytes.length + " bytes long");
//		Log.d(TAG, "Which is AKA: " + new String(subjectBytes));
//		Log.d(TAG, "Its raw bytes are: " + byteArrayToString(subjectBytes));
//		Log.d(TAG, "In base 64: " + Base64.encodeToString(subjectBytes, 0));

		FileOutputStream fos = new FileOutputStream(destination);
		try {
			byte[] plainBytes = eCipher.doFinal(plain.getBytes(ENCODING));
			fos.write(plainBytes);
		} finally {
			if (fos != null) {
				fos.close();
			}
		}
		return true;
	}

	/**
	 * 
	 * @param source
	 * @return Null if nothing could be read
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws BadPaddingException
	 * @throws IllegalBlockSizeException
	 */
	public static String readCipherToPlain(File source)
			throws FileNotFoundException, IOException,
			IllegalBlockSizeException, BadPaddingException {
		long size = source.length();
//		Log.d(TAG, "Source length: " + size);

		if (size > Integer.MAX_VALUE) {
			Log.e(TAG, "Encrypted file too big");
			throw new IOException("File toooooo big");
		}
		if (size <= 0) {
			return null;
		}

		FileInputStream fis = new FileInputStream(source);

		byte[] cipherBytes = new byte[(int) size];
		int resultCode = 0;
		try {
			resultCode = fis.read(cipherBytes);
		} finally {
			if (fis != null) {
				fis.close();
			}
		}
		if (resultCode < 0 || resultCode != cipherBytes.length) {
			Log.e(TAG, "Couldn't read file properly");
			throw new IOException("Error while decrypting file: " + resultCode);
		}

//		Log.d(TAG, "Raw bytes of ciphertext: " + byteArrayToString(cipherBytes));
//		Log.d(TAG, "In base 64: " + Base64.encodeToString(cipherBytes, 0));

		byte[] plainBytes = dCipher.doFinal(cipherBytes);
		return new String(plainBytes, ENCODING);
	}

	/**
	 * Makes a new secret key. Making a new secret key while there exists
	 * another key in the store will overwrite the old key, making any data
	 * encrypted with the old key permanently unreadable.
	 * 
	 * @param pass
	 *            Password with which to protect the keystore
	 * @return The newly generated secret key
	 * @throws GeneralSecurityException
	 * @throws IOException
	 */
	private static SecretKey generateSecretKey(char[] pass)
			throws GeneralSecurityException, IOException {

		byte[] salt = generateSalt();
		SecretKey key = null;
		PBEKeySpec keySpec = new PBEKeySpec(pass, salt, 1024, 256);
		SecretKeyFactory factory = SecretKeyFactory
				.getInstance(FACTORYALGORITHM);
		SecretKey tmp = factory.generateSecret(keySpec);
		key = new SecretKeySpec(tmp.getEncoded(), "AES");

//		SharedPreferences sp = getSharedPreferences();
//		String salt64 = Base64.encodeToString(salt, 0);
//		Editor editor = sp.edit();
//		editor.putString(PREFS_KEY_CRYPTO_SALT, salt64);
//		editor.commit();

		FileOutputStream storeOutStream = null;
		KeyStore store = KeyStore.getInstance("BKS");
		storeOutStream = SecureMemoApp.getInstance().openFileOutput(STORE_NAME,
				Context.MODE_PRIVATE);
		store.load(null, pass);
		store.setKeyEntry(STORE_ALIAS_KEY, key, pass, null);
		store.store(storeOutStream, pass);
		return key;
	}

	/**
	 * Gets a secret key if one exists in the keystore. Returns null if none
	 * available, or if no keystore exists.
	 * 
	 * @param pass
	 *            Password to the keystore
	 * @return Cipher key, or null if none is available
	 * @throws GeneralSecurityException
	 * @throws IOException
	 */
	private static SecretKey getSecretKey(char[] pass)
			throws GeneralSecurityException, IOException {
		FileInputStream storeInStream = null;
		SecretKey key = null;
		try {
			KeyStore store = KeyStore.getInstance("BKS");
			storeInStream = SecureMemoApp.getInstance()
					.openFileInput(STORE_NAME);
			store.load(storeInStream, pass);
			key = (SecretKey) store.getKey(STORE_ALIAS_KEY, pass);
		} catch (FileNotFoundException e) {
			return null;
		} catch (UnrecoverableKeyException e) {
			throw new GeneralSecurityException("Problem reading keystore");
		} catch (NoSuchAlgorithmException e) {
			throw new GeneralSecurityException("Problem reading keystore");
		} catch (CertificateException e) {
			throw new GeneralSecurityException("Problem reading keystore");
		} catch (IOException e) {
			throw new GeneralSecurityException("Problem reading keystore");
		} finally {
			if (storeInStream != null) {
				storeInStream.close();
			}
		}
		return key;
	}

	private static byte[] getIV() {
		SharedPreferences sp = getSharedPreferences();
		if (sp.contains(PREFS_KEY_IV)) {
			String iv64 = sp.getString(PREFS_KEY_IV, null);
			return Base64.decode(iv64, 0);
		}
		return null;
	}

	private static void storeIV(byte[] iv) {
		Editor e = getSharedPreferences().edit();
		e.putString(PREFS_KEY_IV, Base64.encodeToString(iv, 0));
		e.commit();
	}

	/**
	 * Get dem digits, shunn TODO: Only public for testing
	 * 
	 * @param inChars
	 *            Secret to be hashed
	 * @param salt
	 *            A salt to be applied to the password before hashing.
	 * @return A raw byte array representing the hash. Should be encoded as
	 *         necessary.
	 */
	public static byte[] makeHash(char[] inChars, byte[] salt) {
		MessageDigest digest = null;
		try {
			digest = MessageDigest.getInstance("SHA-256");
		} catch (NoSuchAlgorithmException e) {
			Log.e(TAG, "No hashing algorithm found");
			Log.e(TAG, e.getMessage());
			return null;
		}
		byte[] inBytes = charArrayToByteArray(inChars);
		if (digest == null) {
			return null;
		} else {
			digest.reset();
			digest.update(salt);
			byte[] toRet = digest.digest(inBytes);
			inChars = null; /* nullify secret ASAP */
			for (int i = 0; i < 10000; i++) {
				digest.reset();
				toRet = digest.digest(toRet);
			}
			digest.reset();
			return toRet;
		}
	}
	
	public static byte[] makeHashNoSalt(char[] inChars) {
		MessageDigest digest = null;
		try {
			digest = MessageDigest.getInstance("SHA1");
		} catch (NoSuchAlgorithmException e) {
			Log.e(TAG, "No hashing algorithm found");
			Log.e(TAG, e.getMessage());
			return null;
		}
		byte[] inBytes = charArrayToByteArray(inChars);
		if (digest == null) {
			return null;
		} else {
			digest.reset();
			byte[] toRet = digest.digest(inBytes);
			inChars = null; /* nullify secret ASAP */
			for (int i = 0; i < 10000; i++) {
				digest.reset();
				toRet = digest.digest(toRet);
			}
			digest.reset();
			return toRet;
		}
	}
	
	public static byte[] charArrayToByteArray(char[] in) {
		byte[] out = new byte[in.length * 2];
		int i = 0;
		for (char c : in) {
			out[i++] = (byte) (c >> 8);
			out[i++] = (byte) (c & 0xFF);
		}
		return out;

	}
	
	public static byte[] makeHash(int inInt, byte[] salt) {
		char[] transChar = String.valueOf(inInt).toCharArray();
		return makeHash(transChar, salt);
	}

	/**
	 * Generates a random 8-byte salt TODO: Only public for testing
	 * 
	 * @return A byte array containing the random salt
	 * @throws NoSuchAlgorithmException
	 *             If a cryptographic algorithm is invalid
	 */
	public static byte[] generateSalt() throws NoSuchAlgorithmException {
		SecureRandom rng;
		byte[] ret = new byte[8];
		rng = SecureRandom.getInstance("SHA1PRNG");
		rng.nextBytes(ret);
		return ret;
	}

	/**
	 * Useful for debugging
	 * 
	 * @param bytes
	 * @return
	 */
	private static String byteArrayToString(byte[] bytes) {
		String s = "[ ";
		for (byte b : bytes) {
			int i = (int) b;
			s += i + ", ";
		}
		s += " ]";
		return s;
	}

	/**
	 * Convenience method to get the application's shared preference
	 * 
	 * @return Reference to this CompanionApp's shared preferences
	 */
	private static SharedPreferences getSharedPreferences() {
		return SecureMemoApp.getInstance().getSharedPreferences();
	}

}
